<?php

namespace tch\core;

/**
 * 模型基类，主要拥有setter/getter、自动校验、自动填充等通用数据模型特性
 * 将Tp的model一分为二
 */
class Model
{
    
    // ===== 操作状态 =====
    const 
        /**
         * 插入模型数据
         */
        SCENE_INSERT = 1,
    
        /**
         * 更新模型数据
         */
        SCENE_UPDATE = 2,
    
        /**
         * 包含上面两种方式
         */
        SCENE_BOTH = 3;
    
    const 
        /**
         * 必须验证
         */
        MUST_VALIDATE = 1,
        /**
         * 表单存在字段则验证
         */
        EXISTS_VALIDATE = 0,
        /**
         * 表单值不为空则验证
         */
        VALUE_VALIDATE = 2;
    
    /**
     *
     * @var string 模型名称
     */
    protected $name = '';
    
    /**
     *
     * @var string 最近错误信息
     */
    protected $error = '';
    
    /**
     *
     * @var array 字段信息
     */
    protected $fields = array();
    
    /**
     *
     * @var array 数据信息
     */
    protected $data = array();
    
    /**
     *
     * @var array 查询表达式参数
     */
    protected $options = array();
    
    /**
     *
     * @var array 自动验证定义
     */
    protected $rules = array();
    
    /**
     *
     * @var array 各场景自动填充字段值定义
     */
    protected $autoFillings = array();
    
    /**
     *
     * @var array 字段映射定义
     */
    protected $fieldMaps = array();
    
    /**
     *
     * @var array 操作场景定义
     */
    protected $scenes = array();
    
    /**
     *
     * @var bool 是否批处理验证
     */
    protected $batchValidate = false;
    
    /**
     * 构造函数
     * 取得DB类的实例对象 字段检查
     * @param string $name 模型名称
     */
    public function __construct($name = '')
    {
        // 获取模型名称
        if (!empty($name)) {
            $this->name = $name;
        } elseif (empty($this->name)) {
            $this->name = $this->getModelName();
        }
        
        // 子类进行自己的初始化
        $this->initialize();
    }

    /**
     * 子类初始化模型
     */
    protected function initialize()
    {
        
    }

    /**
     * 得到当前的数据对象名称
     * @return string
     */
    public function getModelName()
    {
        if (empty($this->name)) {
            $name = get_class($this);
            $this->name = str_replace('\\', '__', $name);
        }
        return $this->name;
    }

    /**
     * 设置数据对象的值
     * @param string $name 名称
     * @param mixed $value 值
     * @return void
     */
    public function __set($name, $value)
    {
        // 设置数据对象属性
        $this->data[$name] = $value;
    }

    /**
     * 获取数据对象的值
     * @param string $name 名称
     * @return mixed
     */
    public function __get($name)
    {
        return isset($this->data[$name]) ? $this->data[$name] : null;
    }

    /**
     * 检测数据对象的值
     * @param string $name 名称
     * @return boolean
     */
    public function __isset($name)
    {
        return isset($this->data[$name]);
    }

    /**
     * 销毁数据对象的值
     * @param string $name 名称
     * @return void
     */
    public function __unset($name)
    {
        unset($this->data[$name]);
    }

    /**
     * 处理字段映射
     * @param array $data 当前数据
     * @param integer $type 类型 0 写入 1 读取
     * @return array
     */
    public function parseFieldsMap($data, $type = 1)
    {
        // 检查字段映射
        if (!empty($this->fieldMaps)) {
            foreach ($this->fieldMaps as $key => $val) {
                if ($type == 1) { // 读取
                    if (isset($data[$val])) {
                        $data[$key] = $data[$val];
                        unset($data[$val]);
                    }
                } else {
                    if (isset($data[$key])) {
                        $data[$val] = $data[$key];
                        unset($data[$key]);
                    }
                }
            }
        }
        return $data;
    }

    /**
     * 创建数据对象 应用字段映射、字段检测、数据校验、令牌检测、字段数据自动填充
     * 原TP中的create方法
     * @param mixed $data 创建数据
     * @param string $type 状态
     * @return mixed
     */
    public function load($data, $type = '')
    {
        if (is_object($data)) {
            $data = get_object_vars($data);
        }
        // 验证数据
        if (empty($data) || !is_array($data)) {
            $this->error = lang('plugin:tch_core', '_DATA_TYPE_INVALID_');
            return false;
        }

        // 状态
        $type = $type ?: self::SCENE_INSERT;

        // 检查字段映射
        $data = $this->parseFieldsMap($data, 0);

        // 检测提交字段的合法性
        if (isset($this->options['field'])) { // $this->field('field1,field2...')->create()
            $fields = $this->options['field'];
            unset($this->options['field']);
        }
        
        if (isset($fields)) {
            if (is_string($fields)) {
                $fields = explode(',', $fields);
            }
            // 判断令牌验证字段
            if (getglobal('config/db/token_on')) {
                $token_name = getglobal('config/db/token_name');
                if (empty($token_name)) {
                    $token_name = '__hash__';
                }
                $fields[] = $token_name;
            }
            foreach ($data as $key => $val) {
                if (!in_array($key, $fields)) {
                    unset($data[$key]);
                }
            }
        }

        // 数据自动验证
        if (!$this->autoValidate($data, $type))
            return false;

        // 表单令牌验证
        if (!$this->autoCheckToken($data)) {
            $this->error = lang('plugin:tch_core','_TOKEN_ERROR_');
            return false;
        }

        // 创建完成对数据进行自动处理
        $this->autoFill($data, $type);
        // 赋值当前数据对象
        $this->data = $data;
        // 返回创建的数据以供其他调用
        return $data;
    }

    /**
     * 指定查询字段 支持字段排除
     * @param mixed $field
     * @param boolean $except 是否排除 由DB模型子类实现该特性
     * @return $this
     */
    public function field($field, $except = false)
    {
        $this->options['field'] = $field;
        return $this;
    }

    /**
     * 自动表单令牌验证
     * TODO  ajax无刷新多次提交暂不能满足
     * @param array $data
     */
    public function autoCheckToken($data)
    {
        // 支持使用token(false) 关闭令牌验证
        if (isset($this->options['token']) && !$this->options['token'])
            return true;
        if (getglobal('config/db/token_on')) {
            $name = getglobal('config/db/token_name');
            if (empty($name)) {
                $name = '__hash__';
            }
            
            if (!isset($data[$name]) || !isset($_SESSION[$name])) { // 令牌数据无效
                return false;
            }

            // 令牌验证
            list($key, $value) = explode('_', $data[$name]);
            if (isset($_SESSION[$name][$key]) && $value && $_SESSION[$name][$key] === $value) { // 防止重复提交
                unset($_SESSION[$name][$key]); // 验证完成销毁session
                return true;
            }
            // 开启TOKEN重置
            if (getglobal('config/db/token_reset'))
                unset($_SESSION[$name][$key]);
            return false;
        }
        return true;
    }

    /**
     * 使用正则验证数据
     * @param string $value  要验证的数据
     * @param string $rule 验证规则
     * @return boolean
     */
    public function regex($value, $rule)
    {
        $validate = array(
            'require' => '/\S+/',
            'email' => '/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/',
            'url' => '/^http(s?):\/\/(?:[A-za-z0-9-]+\.)+[A-za-z]{2,4}(:\d+)?(?:[\/\?#][\/=\?%\-&~`@[\]\':+!\.#\w]*)?$/',
            'currency' => '/^\d+(\.\d+)?$/',
            'number' => '/^\d+$/',
            'zip' => '/^\d{6}$/',
            'integer' => '/^[-\+]?\d+$/',
            'double' => '/^[-\+]?\d+(\.\d+)?$/',
            'english' => '/^[A-Za-z]+$/',
        );
        // 检查是否有内置的正则表达式
        if (isset($validate[strtolower($rule)]))
            $rule = $validate[strtolower($rule)];
        return preg_match($rule, $value) === 1;
    }

    /**
     * 自动表单处理
     * @param array $data 创建数据 引用传参
     * @param string $type 创建类型
     * @return mixed
     */
    private function autoFill(&$data, $type)
    {
        if (false === $this->options['auto']) {
            // 关闭自动完成
            return $data;
        }
        if (!empty($this->options['auto'])) {
            $_auto = $this->options['auto'];
            unset($this->options['auto']);
        } elseif (!empty($this->autoFillings)) {
            $_auto = $this->autoFillings;
        }
        // 自动填充
        if (isset($_auto)) {
            foreach ($_auto as $auto) {
                // 填充因子定义格式
                // array('field','填充内容','填充条件','附加规则',[额外参数])
                if (empty($auto[2]))
                    $auto[2] = self::SCENE_INSERT; // 默认为新增的时候自动填充
                if ($type == $auto[2] || $auto[2] == self::SCENE_BOTH) {
                    if (empty($auto[3]))
                        $auto[3] = 'string';
                    switch (trim($auto[3])) {
                        case 'function':    //  使用函数进行填充 字段的值作为参数
                        case 'callback': // 使用回调方法
                            $args = isset($auto[4]) ? (array) $auto[4] : array();
                            if (isset($data[$auto[0]])) {
                                array_unshift($args, $data[$auto[0]]);
                            }
                            if ('function' == $auto[3]) {
                                $data[$auto[0]] = call_user_func_array($auto[1], $args);
                            } else {
                                $data[$auto[0]] = call_user_func_array(array(&$this, $auto[1]), $args);
                            }
                            break;
                        case 'field':    // 用其它字段的值进行填充
                            $data[$auto[0]] = $data[$auto[1]];
                            break;
                        case 'ignore': // 为空忽略
                            if ($auto[1] === $data[$auto[0]])
                                unset($data[$auto[0]]);
                            break;
                        case 'string':
                        default: // 默认作为字符串填充
                            $data[$auto[0]] = $auto[1];
                    }
                    if (isset($data[$auto[0]]) && false === $data[$auto[0]])
                        unset($data[$auto[0]]);
                }
            }
        }
        return $data;
    }

    /**
     * 自动表单验证
     * 
     * @param array $data 创建数据
     * @param string $type 创建类型
     * @return boolean
     */
    protected function autoValidate($data, $type)
    {
        if (false === $this->options['validate']) {
            // 关闭自动验证
            return true;
        }
        if (!empty($this->options['validate'])) {
            $_validate = $this->options['validate'];
            unset($this->options['validate']);
        } elseif (!empty($this->rules)) {
            $_validate = $this->rules;
        }
        // 属性验证
        if (isset($_validate)) { // 如果设置了数据自动验证则进行数据验证
            if ($this->batchValidate) { // 重置验证错误信息
                $this->error = array();
            }
            foreach ($_validate as $key => $val) {
                // 验证因子定义格式
                // array(field,rule,message,condition,type,when,params)
                // 判断是否需要执行验证
                if (empty($val[5]) || ( $val[5] == self::SCENE_BOTH && $type < 3 ) || $val[5] == $type) {
                    if (0 == strpos($val[2], '{%') && strpos($val[2], '}'))
                    // 支持提示信息的多语言 使用 {%语言定义} 方式
                        $val[2] = L(substr($val[2], 2, -1));
                    $val[3] = isset($val[3]) ? $val[3] : self::EXISTS_VALIDATE;
                    $val[4] = isset($val[4]) ? $val[4] : 'regex';
                    // 判断验证条件
                    switch ($val[3]) {
                        case self::MUST_VALIDATE:   // 必须验证 不管表单是否有设置该字段
                            if (false === $this->validateField($data, $val))
                                return false;
                            break;
                        case self::VALUE_VALIDATE:    // 值不为空的时候才验证
                            if ('' != trim($data[$val[0]]))
                                if (false === $this->validateField($data, $val))
                                    return false;
                            break;
                        default:    // 默认表单存在该字段就验证
                            if (isset($data[$val[0]]))
                                if (false === $this->validateField($data, $val))
                                    return false;
                    }
                }
            }
            // 批量验证的时候最后返回错误
            if (!empty($this->error))
                return false;
        }
        return true;
    }

    /**
     * 验证表单字段 支持批量验证
     * 如果批量验证返回错误的数组信息
     * 
     * @param array $data 创建数据
     * @param array $val 验证因子
     * @return boolean
     */
    protected function validateField($data, $val)
    {
        if ($this->batchValidate && isset($this->error[$val[0]]))
            return; //当前字段已经有规则验证没有通过
        if (false === $this->validateFieldItem($data, $val)) {
            if ($this->batchValidate) {
                $this->error[$val[0]] = $val[2];
            } else {
                $this->error = $val[2];
                return false;
            }
        }
        return;
    }

    /**
     * 根据验证因子验证字段
     * 
     * @param array $data 创建数据
     * @param array $val 验证因子
     * @return boolean
     */
    protected function validateFieldItem($data, $val)
    {
        switch (strtolower(trim($val[4]))) {
            case 'function':// 使用函数进行验证
            case 'callback':// 调用方法进行验证
                $args = isset($val[6]) ? (array) $val[6] : array();
                if (is_string($val[0]) && strpos($val[0], ','))
                    $val[0] = explode(',', $val[0]);
                if (is_array($val[0])) {
                    // 支持多个字段验证
                    foreach ($val[0] as $field)
                        $_data[$field] = $data[$field];
                    array_unshift($args, $_data);
                } else {
                    array_unshift($args, $data[$val[0]]);
                }
                if ('function' == $val[4]) {
                    return call_user_func_array($val[1], $args);
                } else {
                    return call_user_func_array(array(&$this, $val[1]), $args);
                }
            case 'confirm': // 验证两个字段是否相同
                return $data[$val[0]] == $data[$val[1]];
            default:  // 检查附加规则
                return $this->check($data[$val[0]], $val[1], $val[4]);
        }
    }

    /**
     * 验证数据 支持 in between equal length regex expire ip_allow ip_deny
     * @param string $value 验证数据
     * @param mixed $rule 验证表达式
     * @param string $type 验证方式 默认为正则验证
     * @return boolean
     */
    public function check($value, $rule, $type = 'regex')
    {
        $type = strtolower(trim($type));
        switch ($type) {
            case 'in': // 验证是否在某个指定范围之内 逗号分隔字符串或者数组
            case 'notin':
                $range = is_array($rule) ? $rule : explode(',', $rule);
                return $type == 'in' ? in_array($value, $range) : !in_array($value, $range);
            case 'between': // 验证是否在某个范围
            case 'notbetween': // 验证是否不在某个范围            
                if (is_array($rule)) {
                    $min = $rule[0];
                    $max = $rule[1];
                } else {
                    list($min, $max) = explode(',', $rule);
                }
                return $type == 'between' ? $value >= $min && $value <= $max : $value < $min || $value > $max;
            case 'equal': // 验证是否等于某个值
            case 'notequal': // 验证是否等于某个值            
                return $type == 'equal' ? $value == $rule : $value != $rule;
            case 'length': // 验证长度
                $length = mb_strlen($value, 'utf-8'); // 当前数据长度
                if (strpos($rule, ',')) { // 长度区间
                    list($min, $max) = explode(',', $rule);
                    return $length >= $min && $length <= $max;
                } else {// 指定长度
                    return $length == $rule;
                }
            case 'expire':
                list($start, $end) = explode(',', $rule);
                if (!is_numeric($start))
                    $start = strtotime($start);
                if (!is_numeric($end))
                    $end = strtotime($end);
                return getglobal('timestamp') >= $start && getglobal('timestamp') <= $end;
            case 'ip_allow': // IP 操作许可验证
                return in_array(getglobal('clientip'), explode(',', $rule));
            case 'ip_deny': // IP 操作禁止验证
                return !in_array(getglobal('clientip'), explode(',', $rule));
            case 'regex':
            default:    // 默认使用正则验证 可以使用验证类中定义的验证名称
                // 检查附加规则
                return $this->regex($value, $rule);
        }
    }

    /**
     * 返回模型的错误信息
     * @return string
     */
    public function getError()
    {
        return $this->error;
    }
    
    /**
     * 直接给数据对象赋值 只是单纯赋值
     * @param mixed $data 数据
     * @return $this
     */
    public function data($data = '')
    {
        if ('' === $data && !empty($this->data)) {
            return $this->data;
        }
        if (is_object($data)) {
            $data = get_object_vars($data);
        } elseif (is_string($data)) {
            parse_str($data, $data);
        } elseif (!is_array($data)) {
            throw new Exception(lang('plugin:tch_core', '_DATA_TYPE_INVALID_'));
        }
        $this->data = $data;
        return $this;
    }
    
    /**
     * 设置模型的属性值
     * @param string $name 名称
     * @param mixed $value 值
     * @return $this
     */
    public function setProperty($name, $value)
    {
        if (property_exists($this, $name))
            $this->$name = $value;
        return $this;
    }

    /**
     * 数据读取后的处理
     * 
     * @param array $data 当前数据
     * @return array
     */
    protected function readData($data)
    {
        // 检查字段映射
        if (!empty($this->fieldMaps) && getglobal('config/db/fieldsdatamap')) {
            foreach ($this->fieldMaps as $key => $val) {
                if (isset($data[$val])) {
                    $data[$key] = $data[$val];
                    unset($data[$val]);
                }
            }
        }
        return $data;
    }
}
